#include "StdAfx.h"
#include "ImageKeeper.h"
#include "CommDlg.h"
#include "wincodec.h"
#include <d2d1.h>
#include <dwrite.h>
#include <vector>
#include <assert.h>

static const int kHeightOfLabelStripe = 20;
static const int kHeightOfBottomStripe = kHeightOfLabelStripe + 
	kHeightOfSmoothStripe + 2 * kHeightOfDiscreteStripe;

static const int kYPositionOfLabelStripe = kSideOfMagnifierSquare;
static const int kYPositionOfSmoothStripe = kYPositionOfLabelStripe + kHeightOfLabelStripe;
static const int kYPositionOfDiscreteStripe = kYPositionOfSmoothStripe + kHeightOfSmoothStripe;
static const int kYPositionOfDiscreteFewStripe = kYPositionOfDiscreteStripe + kHeightOfDiscreteStripe;

const float kDeltaAlpha = 0.2f;
const int kNumberOfMagnifiedPixels = 10;
const int kSizeOfMagnifiedPixel = 35;
static const double kDefaultCircleRadius = 100;
static const double kMinimalCircleRadius = 20;
static const double kMaximalCircleRadius = 200;

const double kOffsetToCenter = (kSizeOfMagnifiedPixel * kNumberOfMagnifiedPixels) * 0.5;
static const POINT kUndefinedPoint = { LONG_MAX, LONG_MAX };

static bool sIsPendingUninitializeOfComLibrary = false;

template <typename T>
inline void SafeRelease(T *&p)
{
	if (p)
	{
		p->Release();
		p = nullptr;
	}
}


ImageKeeper::ImageKeeper(void)
	: mD2D1Factory(nullptr), mFormatConverter(nullptr),
	mOriginalRenderTarget(nullptr), mMagnifierRenderTarget(nullptr),
	mOriginalBitmap(nullptr), mMagnifierBitmap(nullptr),
	mAnalyticsBitmap(nullptr), mCircleBitmap(nullptr), mLabelBitmap(nullptr),
	mSmoothStripeBitmap(nullptr),
	mOriginalGreenBrush(nullptr), mMagnifierGreenBrush(nullptr)
{
	mMagnifierWindow = nullptr;
	mCenter.x = mCenter.y = kOffsetToCenter;
	mCircleRadius = kDefaultCircleRadius;
	mModeAtClick = kNoChange;
	mPreviousCentralPixel = kUndefinedPoint;
	mCircleSegmentationThreshold = 0.2f;
	mDegreeForWhichBaseFunctionsWereCalculated = -1; // some impossible value
	mIsCircleBitmapValid = false;

	ClearPerformanceLog();

	HRESULT hr = ::CoInitializeEx(NULL, COINIT_DISABLE_OLE1DDE );
	sIsPendingUninitializeOfComLibrary |= (S_OK == hr || S_FALSE == hr);
}


ImageKeeper::~ImageKeeper(void)
{
	mMagnifierBitmapUpdater.StopRunning();
	mAnalyticsUpdater.StopRunning();
	mCircleUpdater.StopRunning();

	SafeRelease(mD2D1Factory);
	SafeRelease(mFormatConverter);
	SafeRelease(mOriginalRenderTarget);
	SafeRelease(mMagnifierRenderTarget);
	SafeRelease(mOriginalBitmap);
	SafeRelease(mMagnifierBitmap);
	SafeRelease(mAnalyticsBitmap);
	SafeRelease(mCircleBitmap);
	SafeRelease(mLabelBitmap);
	SafeRelease(mSmoothStripeBitmap);
	SafeRelease(mOriginalGreenBrush);
	SafeRelease(mMagnifierGreenBrush);
	if (sIsPendingUninitializeOfComLibrary)
	{
		::CoUninitialize();
		sIsPendingUninitializeOfComLibrary = false;
	}
}

bool ImageKeeper::CreateGraphicsFactories()
{
	HRESULT hr = ::D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &mD2D1Factory);
	if (SUCCEEDED(hr))
	{
		hr = mD2D1Factory->ReloadSystemMetrics();
		if (SUCCEEDED(hr))
		{
			FLOAT dpiX, dpiY;
			mD2D1Factory->GetDesktopDpi(&dpiX, &dpiY);
			mDesktopDPIScale.x = dpiX / 96;
			mDesktopDPIScale.y = dpiY / 96;
		}
	}
	return SUCCEEDED(hr);
}

bool ImageKeeper::RecreateOriginalRenderTarget(HWND inHWnd)
{
	if (nullptr == mOriginalRenderTarget && nullptr != mD2D1Factory)
	{
		RECT rc;
		if (GetClientRect(inHWnd, &rc))
		{
			auto renderTargetProperties = D2D1::RenderTargetProperties();
			renderTargetProperties.usage = D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE;
			auto size = D2D1::SizeU(rc.right, rc.bottom);

			HRESULT hr = mD2D1Factory->CreateHwndRenderTarget(
				renderTargetProperties,
				D2D1::HwndRenderTargetProperties(inHWnd, size),
				&mOriginalRenderTarget);
		}
	}
	return (nullptr != mOriginalRenderTarget);
}

void ImageKeeper::ResizeOriginalRenderTarget(int inCx, int inCy)
{
	if (mOriginalRenderTarget)
	{
		if (S_OK != mOriginalRenderTarget->Resize(D2D1::SizeU(inCx, inCy)))
		{
			mOriginalRenderTarget->Release();
			mOriginalRenderTarget = nullptr;
		}
	}
}

bool ImageKeeper::OpenFile(LPCTSTR inFilePath)
{
	if (nullptr == inFilePath || '\0' == inFilePath[0])
	{
		return false;
	}

	IWICFormatConverter *formatConverter = NULL;

	IWICImagingFactory *factory = NULL;
	HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL,
		CLSCTX_INPROC_SERVER, IID_IWICImagingFactory, (LPVOID*)&factory);
	if (SUCCEEDED(hr))
	{
		IWICBitmapDecoder *decoder = NULL;
		hr = factory->CreateDecoderFromFilename(inFilePath, NULL,
			GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder);
		UINT frameCount = 0;
		if (SUCCEEDED(hr) && S_OK == decoder->GetFrameCount(&frameCount) &&
			0 < frameCount)
		{
			IWICBitmapFrameDecode *frameDecode = NULL;
			hr = decoder->GetFrame(0, &frameDecode);
			if (SUCCEEDED(hr))
			{
				hr = factory->CreateFormatConverter(&formatConverter);
				if (SUCCEEDED(hr))
				{
					hr = formatConverter->Initialize(frameDecode,
						GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone,
						NULL, 0, WICBitmapPaletteTypeCustom);
				}
			}
			SafeRelease(frameDecode);
		}
		SafeRelease(decoder);
	}
	SafeRelease(factory);

	if (SUCCEEDED(hr))
	{
		std::swap(formatConverter, mFormatConverter);
		SafeRelease(mOriginalBitmap);
	}
	SafeRelease(formatConverter);

	ClearPerformanceLog();

	return SUCCEEDED(hr);
}

SIZE ImageKeeper::GetSize() const
{
	UINT cx = 0, cy = 0;
	if (mFormatConverter)
	{
		mFormatConverter->GetSize(&cx, &cy);
		cx = UINT(cx * mDesktopDPIScale.x);
		cy = UINT(cy * mDesktopDPIScale.y);
	}
	return { cx, cy };
}

bool ImageKeeper::DrawOriginal(const POINT& inScrollPosition, double inScale,
	bool inInterpolate, bool inDrawCenter, const RECT& inDirtyRect)
{
	if (nullptr == mOriginalRenderTarget)
	{
		return false;
	}

	if (nullptr == mOriginalBitmap && nullptr != mFormatConverter)
	{
		D2D1_PIXEL_FORMAT pixelFormat = mOriginalRenderTarget->GetPixelFormat();
		HRESULT hr = mOriginalRenderTarget->CreateBitmapFromWicBitmap(mFormatConverter,
			nullptr, &mOriginalBitmap);
	}

	// convert device pixels to points..
	D2D1_RECT_F dirtyRect;
	dirtyRect.left = FLOAT(inDirtyRect.left / mDesktopDPIScale.x);
	dirtyRect.right = FLOAT(inDirtyRect.right / mDesktopDPIScale.x);
	dirtyRect.top = FLOAT(inDirtyRect.top / mDesktopDPIScale.y);
	dirtyRect.bottom = FLOAT(inDirtyRect.bottom / mDesktopDPIScale.y);

	FLOAT scrollPositionX = FLOAT(inScrollPosition.x / mDesktopDPIScale.x);
	FLOAT scrollPositionY = FLOAT(inScrollPosition.y / mDesktopDPIScale.y);

	mOriginalRenderTarget->BeginDraw();
	mOriginalRenderTarget->PushAxisAlignedClip(dirtyRect, D2D1_ANTIALIAS_MODE_ALIASED);

	D2D1_SIZE_F destSize = mOriginalRenderTarget->GetSize();

	SIZE scaledSize = { 0, 0 };// GetSize();
	if (mOriginalBitmap)
	{
		D2D1_SIZE_U size = mOriginalBitmap->GetPixelSize();
		scaledSize.cx = LONG(size.width * inScale);
		scaledSize.cy = LONG(size.height * inScale);
	}

	if (nullptr == mOriginalBitmap || destSize.width + scrollPositionX > scaledSize.cx
		|| destSize.height + scrollPositionY > scaledSize.cy)
	{
		COLORREF backgroundColor = ::GetSysColor(COLOR_WINDOW);
		mOriginalRenderTarget->Clear(D2D1::ColorF(backgroundColor));
	}

	if (mOriginalBitmap)
	{
		D2D1_RECT_F destRectf = { 0, 0, destSize.width, destSize.height };
		double maxRight = scaledSize.cx - scrollPositionX;
		destRectf.right = FLOAT( fmin(destRectf.right, maxRight) );
		double maxBottom = scaledSize.cy - scrollPositionY;
		destRectf.bottom = FLOAT( fmin(destRectf.bottom, maxBottom) );

		if (destRectf.left < destRectf.right && destRectf.top < destRectf.bottom)
		{
			D2D1_RECT_F sourceRectf;
			sourceRectf.left = FLOAT((destRectf.left + scrollPositionX) / inScale);
			sourceRectf.right = FLOAT((destRectf.right + scrollPositionX) / inScale);
			sourceRectf.top = FLOAT((destRectf.top + scrollPositionY) / inScale);
			sourceRectf.bottom = FLOAT((destRectf.bottom + scrollPositionY) / inScale);
			D2D1_BITMAP_INTERPOLATION_MODE mode = (inInterpolate ?
				D2D1_BITMAP_INTERPOLATION_MODE_LINEAR :
				D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR);
			mOriginalRenderTarget->DrawBitmap(mOriginalBitmap, destRectf, 1.0f, mode, sourceRectf);
		}
		if (inDrawCenter)
		{
			FLOAT x = FLOAT(mLastReflectingPoint.x * inScale - scrollPositionX);
			FLOAT y = FLOAT(mLastReflectingPoint.y * inScale - scrollPositionY);
			D2D1_ELLIPSE ellipse = { { x, y }, 2, 2 };
			mOriginalRenderTarget->DrawEllipse(ellipse, GetGreenBrush());
		}
	}

	mOriginalRenderTarget->PopAxisAlignedClip();
	HRESULT hr = mOriginalRenderTarget->EndDraw();

	return SUCCEEDED(hr);
}

RECT ImageKeeper::GetLastReflectingPointArea(const POINT& inScrollPosition,
	double inScale)
{
	RECT r;
	r.left = r.right = ::lround(mLastReflectingPoint.x * inScale * mDesktopDPIScale.x)
		- inScrollPosition.x;
	r.top = r.bottom = ::lround(mLastReflectingPoint.y * inScale * mDesktopDPIScale.y)
		- inScrollPosition.y;
	int halfWidth = int(::ceil(3 * mDesktopDPIScale.x));
	int halfHeight = int(::ceil(3 * mDesktopDPIScale.y));
	::InflateRect(&r, halfWidth, halfHeight);
	return r;
}

void ImageKeeper::SetPixelCountPerMagnifierSide(int inCount)
{
	mPixelCountPerMagnifierSide = inCount;
	ClearPerformanceLog();
	mCircleRadius = kDefaultCircleRadius;
	StartUpdatingMagnifierBitmapData();
}

bool ImageKeeper::DrawMagnifier(const RECT& inDirtyRect)
{
	if (::IsRectEmpty(&inDirtyRect) || !RecreateMagnifierRenderTarget())
	{
		return false;
	}
	UpdateMagnifierBitmap();
	UpdateAnalyticsBitmap();
	UpdateLabelBitmap();

	D2D1_RECT_F dirtyRect;
	dirtyRect.left = FLOAT(inDirtyRect.left / mDesktopDPIScale.x);
	dirtyRect.right = FLOAT(inDirtyRect.right / mDesktopDPIScale.x);
	dirtyRect.top = FLOAT(inDirtyRect.top / mDesktopDPIScale.y);
	dirtyRect.bottom = FLOAT(inDirtyRect.bottom / mDesktopDPIScale.y);

	D2D1_RECT_F areaUnderBitmap;
	areaUnderBitmap.left = FLOAT(max(0, dirtyRect.left));
	areaUnderBitmap.top = FLOAT(max(0, dirtyRect.top));
	areaUnderBitmap.right = FLOAT(min(dirtyRect.right, kSideOfMagnifierSquare));
	areaUnderBitmap.bottom = FLOAT(min(dirtyRect.bottom, kSideOfMagnifierSquare));

	D2D1_RECT_F destinationRect = { 0, 0, FLOAT(kSideOfMagnifierSquare),
		FLOAT(kSideOfMagnifierSquare) };

	mMagnifierRenderTarget->BeginDraw();

	// if not empty rectangle
	if (areaUnderBitmap.left < areaUnderBitmap.right && 
		areaUnderBitmap.top < areaUnderBitmap.bottom)
	{
		mMagnifierRenderTarget->PushAxisAlignedClip(areaUnderBitmap, D2D1_ANTIALIAS_MODE_ALIASED);

		if (mMagnifierBitmap)
		{
			// check intersection of dirtyRect and destiantionRect
			if (dirtyRect.left < destinationRect.right && dirtyRect.right > destinationRect.left &&
				dirtyRect.top < destinationRect.bottom && dirtyRect.bottom > destinationRect.top)
			{
				if (mShowAnalytic && nullptr != mAnalyticsBitmap && mIsAnalyticsBitmapValid)
				{
					mMagnifierRenderTarget->DrawBitmap(mAnalyticsBitmap, destinationRect);
				}
				else
				{
					D2D1_RECT_F sourceRect = { 0, 0, FLOAT(mPixelCountPerMagnifierSide),
						FLOAT(mPixelCountPerMagnifierSide) };
					mMagnifierRenderTarget->DrawBitmap(mMagnifierBitmap, destinationRect, 1,
						D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, sourceRect);
				}
			}
		}
		else
		{
			// ?
			mMagnifierRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
		}



		const FLOAT scale = FLOAT(kSideOfMagnifierSquare) / mPixelCountPerMagnifierSide;
		D2D1_POINT_2F center = { FLOAT(mCenter.x), FLOAT(mCenter.y) };

		// if dirtyRect intersects with ellipse
		if (dirtyRect.left < center.x + 3 && dirtyRect.right > center.x - 3 &&
			dirtyRect.top < center.y + 3 && dirtyRect.bottom > center.y - 3)
		{
			D2D1_ELLIPSE ellipse = { center, 2, 2 };
			mMagnifierRenderTarget->DrawEllipse(ellipse, GetMagnifierGreenBrush());
		}

		UpdateCircleBitmap();

		bool isCircleDrawn = false;
		if (mIsCircleBitmapValid)
		{
			if (kCircleAppearanceSmoothColors == mCircleAppearance)
			{
				if (nullptr != mCircleBitmap)
				{
					mMagnifierRenderTarget->DrawBitmap(mCircleBitmap, destinationRect);
					isCircleDrawn = true;
				}
			}
			else if (kCircleAppearanceDiscreteColors == mCircleAppearance)
			{
				DrawSegmentedCircle(mCircleDivider);
				isCircleDrawn = true;
			}
			else if (kCircleAppearanceFewDiscreteColors == mCircleAppearance)
			{
				DrawSegmentedCircle(mCircleDividerFew);
				isCircleDrawn = true;
			}
		}		

		if (!isCircleDrawn)
		{
			D2D1_ELLIPSE ellipse = { center, FLOAT(mCircleRadius), FLOAT(mCircleRadius) };
			mMagnifierRenderTarget->DrawEllipse(ellipse, GetMagnifierGreenBrush());
		}

		mMagnifierRenderTarget->PopAxisAlignedClip();
	}

	// draw bottom strip
	if (inDirtyRect.bottom > kYPositionOfLabelStripe)
	{
		mMagnifierRenderTarget->PushAxisAlignedClip(dirtyRect, D2D1_ANTIALIAS_MODE_ALIASED);
		if (mLabelBitmap)
		{
			static const D2D1_RECT_F destinationRect = { 0, kYPositionOfLabelStripe,
				kSideOfMagnifierSquare, kYPositionOfLabelStripe + kHeightOfLabelStripe };
			static const D2D1_RECT_F sourceRect = { 0, 0,
				kSideOfMagnifierSquare, kHeightOfLabelStripe };
			mMagnifierRenderTarget->DrawBitmap(mLabelBitmap, destinationRect, 1,
				D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, sourceRect);
		}

		if (mSmoothStripeBitmap && mIsCircleBitmapValid)
		{
			static const D2D1_RECT_F destinationRect = { 0, kYPositionOfSmoothStripe,
				kSideOfMagnifierSquare, kYPositionOfSmoothStripe + kHeightOfSmoothStripe };
			static const D2D1_RECT_F sourceRect = { 0, 0,
				kSideOfMagnifierSquare, kHeightOfSmoothStripe };
			mMagnifierRenderTarget->DrawBitmap(mSmoothStripeBitmap, destinationRect, 1,
				D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, sourceRect);

			DrawDiscreteColorsStripe(mCircleDivider, kYPositionOfDiscreteStripe);
			DrawDiscreteColorsStripe(mCircleDividerFew, kYPositionOfDiscreteFewStripe);
		}
		
		static const D2D1_POINT_2F pt1 = { 0, kYPositionOfDiscreteFewStripe };
		static const D2D1_POINT_2F pt2 = { kSideOfMagnifierSquare,
			kYPositionOfDiscreteFewStripe };
		ID2D1SolidColorBrush* brush = nullptr;
		mMagnifierRenderTarget->CreateSolidColorBrush(D2D1::ColorF(0), &brush);

		mMagnifierRenderTarget->DrawLine(pt1, pt2, brush, 0.5f);
		SafeRelease(brush);


		mMagnifierRenderTarget->PopAxisAlignedClip();
	}

	HRESULT hr = mMagnifierRenderTarget->EndDraw();
	if (hr == D2DERR_RECREATE_TARGET)
	{
		hr = S_OK;
		SafeRelease(mMagnifierRenderTarget);
		SafeRelease(mMagnifierBitmap);
		SafeRelease(mAnalyticsBitmap);
		SafeRelease(mCircleBitmap);
		SafeRelease(mLabelBitmap);
		SafeRelease(mSmoothStripeBitmap);
		SafeRelease(mMagnifierGreenBrush);
	}
	return SUCCEEDED(hr);
}

void ImageKeeper::SetDegree(int inDegree)
{
	mAnalyticsUpdater.Invalidate();
	mDegree = inDegree;
	ClearPerformanceLog();

	if (auto ptr = mMagnifierBitmapUpdater.BeginRead())
	{
		int updateCount = mMagnifierBitmapUpdater.GetTotalUpdateCount();
		mMagnifierBitmapUpdater.EndRead(ptr);
		StartUpdatingAnalytics(updateCount);
	}
}

void ImageKeeper::SetCircleAppearance(CircleAppearanceEnum inAppearance)
{
	mCircleAppearance = inAppearance;
	::InvalidateRect(mMagnifierWindow, nullptr, false);
}

// ------------------------------------------------------

void ImageKeeper::SetReflectingPointFromDevicePixels(double inX, double inY)
{
	if (NULL == mMagnifierWindow)
	{
		return;
	}

	mLastReflectingPoint.x = inX / mDesktopDPIScale.x;
	mLastReflectingPoint.y = inY / mDesktopDPIScale.y;
	POINT centralPixel = { LONG(::floor(mLastReflectingPoint.x)), 
		LONG(::floor(mLastReflectingPoint.y)) };

	double scale = double(kSideOfMagnifierSquare) / mPixelCountPerMagnifierSide;
	int pixelPosition = mPixelCountPerMagnifierSide / 2;
	double newCenterX = scale * (pixelPosition + mLastReflectingPoint.x - centralPixel.x);
	double newCenterY = scale * (pixelPosition + mLastReflectingPoint.y - centralPixel.y);
	bool isNeedToRefreshCircle = (::fabs(newCenterX - mCenter.x) +
		::fabs(newCenterY - mCenter.y) >= 1.0);
	mCenter.x = newCenterX;
	mCenter.y = newCenterY;

	if (centralPixel.x != mPreviousCentralPixel.x || centralPixel.y != mPreviousCentralPixel.y)
	{
		mPreviousCentralPixel = centralPixel;
		StartUpdatingMagnifierBitmapData();
	}
	else if (isNeedToRefreshCircle)
	{
		StartUpdatingCircle();
		::InvalidateRect(mMagnifierWindow, nullptr, false); // current position
	}
}

void ImageKeeper::GetReflectingPoint(double& outX, double& outY) const
{
	outX = mLastReflectingPoint.x;
	outY = mLastReflectingPoint.y;
}

LPCTSTR ImageKeeper::GetMagnifierCursorShape(int inX, int inY) const
{
	switch (GetChangeMode(inX, inY))
	{
	case kMove: return IDC_SIZEALL;
	case kResize:
	{
		double x = inX / mDesktopDPIScale.x;
		double y = inY / mDesktopDPIScale.y;
		double angle = ::atan2(y - mCenter.y, x - mCenter.x);
		int index = ::lround(4 * angle / 3.141592) & 3;

		static const LPCTSTR kShapes[] = { IDC_SIZEWE, IDC_SIZENWSE, IDC_SIZENS, IDC_SIZENESW };
		return kShapes[index];
	}
	default: return IDC_ARROW;
	};
}

void ImageKeeper::SetInitialPoint(double inX, double inY)
{
	mPrevMagnifierPoint.x = inX / mDesktopDPIScale.x;
	mPrevMagnifierPoint.y = inY / mDesktopDPIScale.y;
	mModeAtClick = GetChangeMode(inX, inY);
}

void ImageKeeper::ChangeCircle(double inNewX, double inNewY)
{
	double newX = inNewX / mDesktopDPIScale.x;
	double newY = inNewY / mDesktopDPIScale.y;

	// restrict action to the area of square
	if (newX < 0 || newX > kSideOfMagnifierSquare ||
		newY < 0 || newY > kSideOfMagnifierSquare)
	{
		return;
	}

	if (kMove == mModeAtClick)
	{
		MoveCircleBy(newX - mPrevMagnifierPoint.x, newY - mPrevMagnifierPoint.y);
	}
	else if (kResize == mModeAtClick)
	{
		double oldDx = mPrevMagnifierPoint.x - mCenter.x;
		double oldDy = mPrevMagnifierPoint.y - mCenter.y;
		double oldDistQ = oldDx * oldDx + oldDy * oldDy;
		if (oldDistQ < 1)
		{
			return;
		}
		double dx = newX - mCenter.x;
		double dy = newY - mCenter.y;
		double distQ = dx * dx + dy * dy;
		double radius = mCircleRadius * ::sqrt(distQ / oldDistQ);
		const double minimalQ = kMinimalCircleRadius * kMinimalCircleRadius;
		const double maximalQ = kMaximalCircleRadius * kMaximalCircleRadius;
		if (kMinimalCircleRadius > radius || radius > kMaximalCircleRadius)
		{
			return;
		}

		double maxR = max(radius, mCircleRadius) + 2.0;
		mCircleRadius = radius;
		StartUpdatingCircle();

		if (kCircleAppearanceGreenLine != mCircleAppearance)
		{
			maxR += 10;
		}

		RECT r;
		r.left = LONG(::floor((mCenter.x - maxR) * mDesktopDPIScale.x));
		r.right = LONG(::ceil((mCenter.x + maxR + 1) * mDesktopDPIScale.x));
		r.top = LONG(::floor((mCenter.y - maxR) * mDesktopDPIScale.y));
		r.bottom = LONG(::ceil((mCenter.y + maxR + 1) * mDesktopDPIScale.y));
		::InvalidateRect(mMagnifierWindow, &r, false);
	}
	mPrevMagnifierPoint = { newX, newY };
}

void ImageKeeper::ChangeCircleRadiusBy(double inRadiusDelta)
{
	double radiusDelta = inRadiusDelta;
	double radius = mCircleRadius + radiusDelta;
	if (0 < radiusDelta)
	{
		radius = min(radius, kMaximalCircleRadius);
	}
	else
	{
		radius = max(radius, kMinimalCircleRadius);
	}
	if (::fabs(radius - mCircleRadius) > 0.1)
	{
		mCircleRadius = radius;
		StartUpdatingCircle();
		::InvalidateRect(mMagnifierWindow, nullptr, false);
	}
}

void ImageKeeper::MoveCircleBy(double inDx, double inDy)
{
	double left = min(mCenter.x, mCenter.x + inDx);
	double right = max(mCenter.x, mCenter.x + inDx);
	double top = min(mCenter.y, mCenter.y + inDy);
	double bottom = max(mCenter.y, mCenter.y + inDy);

	RECT r;
	r.left = int(::floor((left - mCircleRadius) * mDesktopDPIScale.x)) - 1;
	r.right = int(::ceil((right + mCircleRadius) * mDesktopDPIScale.x)) + 1;
	r.top = int(::floor((top - mCircleRadius) * mDesktopDPIScale.y)) - 1;
	r.bottom = int(::ceil((bottom + mCircleRadius) * mDesktopDPIScale.y)) + 1;

	mCenter.x += inDx;
	mCenter.y += inDy;
	StartUpdatingCircle();
	::InvalidateRect(mMagnifierWindow, &r, false);

	// move the center on the main window
	double scale = double(kSideOfMagnifierSquare) / mPixelCountPerMagnifierSide;
	int halfSide = mPixelCountPerMagnifierSide / 2;
	mLastReflectingPoint.x = mPreviousCentralPixel.x - halfSide
		+ (mCenter.x / scale);
	mLastReflectingPoint.y = mPreviousCentralPixel.y - halfSide
		+ (mCenter.y / scale);
}

bool ImageKeeper::IsCenterMoving() const
{
	return (kMove == mModeAtClick);
}

SIZE ImageKeeper::GetMagnifierSize() const
{
	SIZE z;
	z.cx = LONG(kSideOfMagnifierSquare * mDesktopDPIScale.x);
	z.cy = LONG((kSideOfMagnifierSquare + kHeightOfBottomStripe) * mDesktopDPIScale.y);
	return z;
}

void ImageKeeper::SetMagnifierWindow(HWND inHwnd)
{
	mMagnifierWindow = inHwnd;
}

HWND ImageKeeper::GetMagnifierWindow()
{
	return mMagnifierWindow;
}

bool ImageKeeper::IsPointInMagnifierSquare(int inX, int inY) const
{
	return inY < (kSideOfMagnifierSquare * mDesktopDPIScale.y);
}



void ImageKeeper::ShowAnalytic(bool inShow)
{
	mShowAnalytic = inShow;
	if (mAnalyticsNeedsUpdate || nullptr != mAnalyticsBitmap)
	{
		::InvalidateRect(mMagnifierWindow, nullptr, false);
	}
}

std::string ImageKeeper::GetPerformanceLog()
{
	
	if (0 == mNumberOfSimilarPolynomialCalculations)
	{
		return "No data.";
	}

	LARGE_INTEGER frequency;
	if (!::QueryPerformanceFrequency(&frequency) || 0 == frequency.QuadPart)
	{
		return "Problem: high-resolution performance counter is not working.";
	}

	double scaleToMilliseconds = 1000.0 / frequency.QuadPart;

	std::string result = "Processing time in milliseconds:\n\n";
	result += "Last calculation of polynomial     \t" +
		std::to_string(mLastPolynomialCalculationTime * scaleToMilliseconds) + "\n";

	double t = mCumulativePolynomialCalculationTime * scaleToMilliseconds / 
		mNumberOfSimilarPolynomialCalculations;
	result += "Average calculation of polinomial   \t" + std::to_string(t) + "\n";

	if (mNumberOfSimilarCircleCalculations)
	{ 
		t = mLastCircleCalculationTime * scaleToMilliseconds;
		result += "\nLast calculation of circle  \t\t" + std::to_string(t) + "\n";

		t = mCumulativeCircleCalculationTime * scaleToMilliseconds / 
			mNumberOfSimilarCircleCalculations;
		result += "Average calculation of circle \t\t" + std::to_string(t) + "\n";
	}

	if (mNumberOfCircleDivisions)
	{
		std::string comparisonString =
			mMaxIterationsForCircleDivision >= kLimitForIterations ?
			">= " : "";

		result += "\n-----------------------------\n"
			"Iteration count:\n\n";
		result += "Maximum for circle division\t" + comparisonString +
			std::to_string(mMaxIterationsForCircleDivision) + "\n";

		float average = float(mCumulativeIterationsForCircleDivision) /
			mNumberOfCircleDivisions;
		result += "Average for circle division\t " + std::to_string(average) + "\n";
	}
	return result;
}

//----------------------------------------------------------


ImageKeeper::ChangeMode ImageKeeper::GetChangeMode(double inX, double inY) const
{
	double dx = (inX / mDesktopDPIScale.x) - mCenter.x;
	double dy = (inY / mDesktopDPIScale.y) - mCenter.y;
	double dist = ::sqrt(dx * dx + dy * dy);
	if (dist < 0.7 * mCircleRadius)
	{
		return kMove;
	}
	else if (dist < 1.1 * mCircleRadius)
	{
		return kResize;
	}
	return kNoChange;
}

ID2D1SolidColorBrush* ImageKeeper::GetGreenBrush()
{
	if (nullptr == mOriginalGreenBrush && nullptr != mOriginalRenderTarget)
	{
		mOriginalRenderTarget->CreateSolidColorBrush(
			D2D1::ColorF(D2D1::ColorF::Green), &mOriginalGreenBrush);
	}
	return mOriginalGreenBrush;
}

ID2D1SolidColorBrush* ImageKeeper::GetMagnifierGreenBrush()
{
	if (nullptr == mMagnifierGreenBrush && nullptr != mMagnifierRenderTarget)
	{
		mMagnifierRenderTarget->CreateSolidColorBrush(
			D2D1::ColorF(D2D1::ColorF::Green), &mMagnifierGreenBrush);
	}
	return mMagnifierGreenBrush;
}

bool ImageKeeper::RecreateMagnifierRenderTarget()
{
	if (nullptr == mMagnifierRenderTarget && nullptr != mMagnifierWindow &&
		nullptr != mD2D1Factory)
	{
		RECT rc;
		if (GetClientRect(mMagnifierWindow, &rc))
		{
			auto renderTargetProperties = D2D1::RenderTargetProperties();
			renderTargetProperties.usage = D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE;
			auto size = D2D1::SizeU(rc.right, rc.bottom);

			HRESULT hr = mD2D1Factory->CreateHwndRenderTarget(
				renderTargetProperties,
				D2D1::HwndRenderTargetProperties(mMagnifierWindow, size),
				&mMagnifierRenderTarget);
		}
	}
	return (nullptr != mMagnifierRenderTarget);
}

IDWriteTextFormat* ImageKeeper::DefaultTextFormat()
{
	IDWriteTextFormat* resultTextFormat = nullptr;
	IDWriteFactory* writeFactory = nullptr;

	HRESULT hr = ::DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
		__uuidof(IDWriteFactory), reinterpret_cast<IUnknown**>(&writeFactory));
	if (SUCCEEDED(hr))
	{
		hr = writeFactory->CreateTextFormat(L"Times New Roman", nullptr,
			DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, 
			DWRITE_FONT_STRETCH_NORMAL, 18, L"en-us", &resultTextFormat);
	}
	SafeRelease(writeFactory);

	return resultTextFormat;
}

void ImageKeeper::DrawDiscreteColorsStripe(const CircleDivider& inDivider, int inPositionY)
{
	D2D1_RECT_F rect;
	rect.left = 0;
	rect.top = float(inPositionY);
	rect.bottom = float(inPositionY + kHeightOfDiscreteStripe);

	ID2D1SolidColorBrush* brush = nullptr;
	D2D1::ColorF color(0);
	mMagnifierRenderTarget->CreateSolidColorBrush(color, &brush);
	int numberOfColors = inDivider.GetNumberOfSegments();
	if (numberOfColors <= 1)
	{
		assert(1 == numberOfColors);
		COLORREF colorRef = inDivider.GetColorAtIndex(0);
		rect.right = kSideOfMagnifierSquare;
		brush->SetColor(D2D1::ColorF(colorRef));
		mMagnifierRenderTarget->FillRectangle(rect, brush);
		brush->Release();
		return;
	}
	float angle = inDivider.GetAngleAtIndex(0);
	const float scaleAngleToWindow = float(kStripeInterval2pi / (2 * M_PI));
	for (int i = 0; i < numberOfColors; ++i)
	{
		float nextAngle = inDivider.GetAngleAtIndex(i + 1);
		rect.left = kPositionOfZeroAngle + angle * scaleAngleToWindow;
		rect.right = kPositionOfZeroAngle + nextAngle * scaleAngleToWindow;
		COLORREF colorRef = inDivider.GetColorAtIndex(i);
		brush->SetColor(D2D1::ColorF(colorRef));
		mMagnifierRenderTarget->FillRectangle(rect, brush);

		if (rect.right > kStripeInterval2pi)
		{
			D2D1_RECT_F rectOfLeftEdge = rect;
			rectOfLeftEdge.right -= kStripeInterval2pi;
			rectOfLeftEdge.left = max(0, rect.left - kStripeInterval2pi);
			mMagnifierRenderTarget->FillRectangle(rectOfLeftEdge, brush);
		}
		if (rect.left < kSideOfMagnifierSquare - kStripeInterval2pi)
		{
			D2D1_RECT_F rectOfRightEdge = rect;
			rectOfRightEdge.left += kStripeInterval2pi;
			rectOfRightEdge.right = min(rect.right + kStripeInterval2pi,
				kSideOfMagnifierSquare);
			mMagnifierRenderTarget->FillRectangle(rectOfRightEdge, brush);
		}

		angle = nextAngle;
	}
	
	SafeRelease(brush);
}

void ImageKeeper::DrawSegmentedCircle(const CircleDivider& inCircleDivider)
{
	ID2D1SolidColorBrush* brush = nullptr;
	HRESULT hr = mMagnifierRenderTarget->CreateSolidColorBrush(D2D1::ColorF(0), &brush);
	if (!SUCCEEDED(hr))
	{
		return;
	}
	int numberOfSegments = inCircleDivider.GetNumberOfSegments();
	if (1 >= numberOfSegments)
	{
		assert(1 == numberOfSegments);
		COLORREF colorRef = inCircleDivider.GetColorAtIndex(0);
		brush->SetColor(D2D1::ColorF(colorRef));
		DrawCircleInSolidColor(brush);
		SafeRelease(brush);
		return;
	}


	D2D1_POINT_2F center = { FLOAT(mCenter.x), FLOAT(mCenter.y) };
	FLOAT r1 = FLOAT(mCircleRadius - 10);
	FLOAT r2 = FLOAT(mCircleRadius + 10);
	for (int i = 0; i < numberOfSegments; ++i)
	{
		// create segment as geometry
		ID2D1PathGeometry* path = nullptr;
		hr = mD2D1Factory->CreatePathGeometry(&path);
		ID2D1GeometrySink* sink = nullptr;
		if (SUCCEEDED(hr))
		{
			hr = path->Open(&sink);
		}
		if (SUCCEEDED(hr))
		{
			float angle1 = inCircleDivider.GetAngleAtIndex(i);
			float angle2 = inCircleDivider.GetAngleAtIndex(i + 1);

			D2D1_POINT_2F point;
			point.x = center.x + r2 * cos(angle1);
			point.y = center.y - r2 * sin(angle1);

			sink->BeginFigure(point, D2D1_FIGURE_BEGIN_FILLED);

			D2D1_ARC_SEGMENT arc;
			arc.point.x = center.x + r2 * cos(angle2);
			arc.point.y = center.y - r2 * sin(angle2);
			arc.size = { r2, r2 };
			arc.rotationAngle = 0;
			arc.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE;
			arc.arcSize = (angle2 - angle1 < M_PI ? 
				D2D1_ARC_SIZE_SMALL : D2D1_ARC_SIZE_LARGE);
			sink->AddArc(&arc);
			point.x = center.x + r1 * cos(angle2);
			point.y = center.y - r1 * sin(angle2);
			sink->AddLine(point);
			arc.point.x = center.x + r1 * cos(angle1);
			arc.point.y = center.y - r1 * sin(angle1);
			arc.size = { r1, r1 };
			arc.sweepDirection = D2D1_SWEEP_DIRECTION_CLOCKWISE;
			sink->AddArc(&arc);

			sink->EndFigure(D2D1_FIGURE_END_CLOSED);
			sink->Close();

			COLORREF colorRef = inCircleDivider.GetColorAtIndex(i);
			brush->SetColor(D2D1::ColorF(colorRef));
			mMagnifierRenderTarget->FillGeometry(path, brush);
		}

		SafeRelease(sink);
		SafeRelease(path);
	}
	SafeRelease(brush);
}
void ImageKeeper::DrawCircleInSolidColor(ID2D1SolidColorBrush* inBrush)
{
	D2D1_ELLIPSE ellipse;
	ellipse.point.x = FLOAT(mCenter.x);
	ellipse.point.y = FLOAT(mCenter.y);
	ellipse.radiusX = ellipse.radiusY = FLOAT(mCircleRadius);

	ID2D1EllipseGeometry* circle = nullptr;
	HRESULT hr = mD2D1Factory->CreateEllipseGeometry(ellipse, &circle);
	if (SUCCEEDED(hr))
	{
		mMagnifierRenderTarget->DrawGeometry(circle, inBrush, 20.0);
	}
	SafeRelease(circle);
}

void ImageKeeper::ClearPerformanceLog()
{
	mNumberOfSimilarPolynomialCalculations = 0;
	mNumberOfSimilarCircleCalculations = 0;
	mCumulativePolynomialCalculationTime = 0;
	mCumulativeCircleCalculationTime = 0;

	mNumberOfCircleDivisions = 0;
	mMaxIterationsForCircleDivision = 0;
	mCumulativeIterationsForCircleDivision = 0;
}

void ImageKeeper::UpdateLabelBitmap()
{
	if (nullptr != mLabelBitmap || nullptr == mMagnifierBitmap
		|| nullptr == mMagnifierRenderTarget)
	{
		return;
	}

	ID2D1BitmapRenderTarget* renderTarget = nullptr;
	D2D1_SIZE_F size = { kSideOfMagnifierSquare, kHeightOfLabelStripe };

	HRESULT hr = mMagnifierRenderTarget->CreateCompatibleRenderTarget(size,
		&renderTarget);

	ID2D1LinearGradientBrush* gradientBrush = nullptr;
	if (SUCCEEDED(hr))
	{
		ID2D1GradientStopCollection *stopCollection = nullptr;
		D2D1_GRADIENT_STOP gradientStops[] = {
			{ 0, D2D1::ColorF(0xFFFF80) }, 
			{ 0.25f, D2D1::ColorF(0xFFFFC0) },
			{ 1, D2D1::ColorF(0xFFFFFF) }
		};
		hr = mMagnifierRenderTarget->CreateGradientStopCollection(
			gradientStops, 3, &stopCollection);
		if (SUCCEEDED(hr))
		{
			static D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES gp = {
				{ 0, 0 }, {0, kHeightOfLabelStripe }
			};
			hr = mMagnifierRenderTarget->CreateLinearGradientBrush(gp,
				stopCollection, &gradientBrush);
		}
		SafeRelease(stopCollection);
	}
	if (SUCCEEDED(hr))
	{
		renderTarget->BeginDraw();
		static const D2D1_RECT_F rect =
			{ 0, 0, kSideOfMagnifierSquare, kHeightOfLabelStripe };
		renderTarget->FillRectangle(rect, gradientBrush);

		ID2D1SolidColorBrush* blackBrush = nullptr;

		hr = renderTarget->CreateSolidColorBrush(
			D2D1::ColorF(D2D1::ColorF::Black), &blackBrush);
		if (SUCCEEDED(hr))
		{
			D2D1_POINT_2F pt0 = { kPositionOfZeroAngle, 0 };
			D2D1_POINT_2F pt1 = { kPositionOfZeroAngle, kHeightOfLabelStripe };
			renderTarget->DrawLine(pt0, pt1, blackBrush);

			pt0.x = pt1.x = kPositionOf2PiAngle;
			renderTarget->DrawLine(pt0, pt1, blackBrush);

			if (IDWriteTextFormat* textFormat = DefaultTextFormat())
			{
				static const int kTextInset = 5;
				D2D1_RECT_F textRect = { kPositionOfZeroAngle + kTextInset, 1, 
					kPositionOfZeroAngle + kTextInset + 50, kHeightOfLabelStripe };
				renderTarget->DrawTextW(L"0", 1, textFormat, textRect, blackBrush);

				textRect.left = kPositionOf2PiAngle + kTextInset;
				textRect.right = kSideOfMagnifierSquare;
				renderTarget->DrawTextW(L"2\u03C0", 2, textFormat, textRect, blackBrush);

				ID2D1SolidColorBrush* titleBrush = nullptr;
				hr = renderTarget->CreateSolidColorBrush(
					D2D1::ColorF(D2D1::ColorF::Gray), &titleBrush);
				if (SUCCEEDED(hr))
				{
					textRect.left = 130;
					textRect.right = 300;
					textRect.top = -3;
					const WCHAR title[] = L"circle unrolled";
					renderTarget->DrawTextW(title, ::wcslen(title), textFormat,
						textRect, titleBrush);
				}
				SafeRelease(titleBrush);

				textFormat->Release();
			}

		}
		SafeRelease(blackBrush);

		hr = renderTarget->EndDraw();
	}
	SafeRelease(gradientBrush);
	if (SUCCEEDED(hr))
	{
		hr = renderTarget->GetBitmap(&mLabelBitmap);
		if (!SUCCEEDED(hr))
		{
			SafeRelease(mLabelBitmap);
		}
	}

	SafeRelease(renderTarget);
}
